"use strict";
const ansiStyles = require("ansi-styles");
const { stdout: stdoutColor, stderr: stderrColor } = require("supports-color");
const { stringReplaceAll, stringEncaseCRLFWithFirstIndex } = require("./util");

const { isArray } = Array;

// `supportsColor.level` → `ansiStyles.color[name]` mapping
const levelMapping = ["ansi", "ansi", "ansi256", "ansi16m"];

const styles = Object.create(null);

const applyOptions = (object, options = {}) => {
  if (
    options.level &&
    !(
      Number.isInteger(options.level) &&
      options.level >= 0 &&
      options.level <= 3
    )
  ) {
    throw new Error("The `level` option should be an integer from 0 to 3");
  }

  // Detect level if not set manually
  const colorLevel = stdoutColor ? stdoutColor.level : 0;
  object.level = options.level === undefined ? colorLevel : options.level;
};

class ChalkClass {
  constructor(options) {
    // eslint-disable-next-line no-constructor-return
    return chalkFactory(options);
  }
}

const chalkFactory = (options) => {
  const chalk = {};
  applyOptions(chalk, options);

  chalk.template = (...arguments_) => chalkTag(chalk.template, ...arguments_);

  Object.setPrototypeOf(chalk, Chalk.prototype);
  Object.setPrototypeOf(chalk.template, chalk);

  chalk.template.constructor = () => {
    throw new Error(
      "`chalk.constructor()` is deprecated. Use `new chalk.Instance()` instead."
    );
  };

  chalk.template.Instance = ChalkClass;

  return chalk.template;
};

function Chalk(options) {
  return chalkFactory(options);
}

for (const [styleName, style] of Object.entries(ansiStyles)) {
  styles[styleName] = {
    get() {
      const builder = createBuilder(
        this,
        createStyler(style.open, style.close, this._styler),
        this._isEmpty
      );
      Object.defineProperty(this, styleName, { value: builder });
      return builder;
    },
  };
}

styles.visible = {
  get() {
    const builder = createBuilder(this, this._styler, true);
    Object.defineProperty(this, "visible", { value: builder });
    return builder;
  },
};

const usedModels = [
  "rgb",
  "hex",
  "keyword",
  "hsl",
  "hsv",
  "hwb",
  "ansi",
  "ansi256",
];

for (const model of usedModels) {
  styles[model] = {
    get() {
      const { level } = this;
      return function (...arguments_) {
        const styler = createStyler(
          ansiStyles.color[levelMapping[level]][model](...arguments_),
          ansiStyles.color.close,
          this._styler
        );
        return createBuilder(this, styler, this._isEmpty);
      };
    },
  };
}

for (const model of usedModels) {
  const bgModel = "bg" + model[0].toUpperCase() + model.slice(1);
  styles[bgModel] = {
    get() {
      const { level } = this;
      return function (...arguments_) {
        const styler = createStyler(
          ansiStyles.bgColor[levelMapping[level]][model](...arguments_),
          ansiStyles.bgColor.close,
          this._styler
        );
        return createBuilder(this, styler, this._isEmpty);
      };
    },
  };
}

const proto = Object.defineProperties(() => {}, {
  ...styles,
  level: {
    enumerable: true,
    get() {
      return this._generator.level;
    },
    set(level) {
      this._generator.level = level;
    },
  },
});

const createStyler = (open, close, parent) => {
  let openAll;
  let closeAll;
  if (parent === undefined) {
    openAll = open;
    closeAll = close;
  } else {
    openAll = parent.openAll + open;
    closeAll = close + parent.closeAll;
  }

  return {
    open,
    close,
    openAll,
    closeAll,
    parent,
  };
};

const createBuilder = (self, _styler, _isEmpty) => {
  const builder = (...arguments_) => {
    if (isArray(arguments_[0]) && isArray(arguments_[0].raw)) {
      // Called as a template literal, for example: chalk.red`2 + 3 = {bold ${2+3}}`
      return applyStyle(builder, chalkTag(builder, ...arguments_));
    }

    // Single argument is hot path, implicit coercion is faster than anything
    // eslint-disable-next-line no-implicit-coercion
    return applyStyle(
      builder,
      arguments_.length === 1 ? "" + arguments_[0] : arguments_.join(" ")
    );
  };

  // We alter the prototype because we must return a function, but there is
  // no way to create a function with a different prototype
  Object.setPrototypeOf(builder, proto);

  builder._generator = self;
  builder._styler = _styler;
  builder._isEmpty = _isEmpty;

  return builder;
};

const applyStyle = (self, string) => {
  if (self.level <= 0 || !string) {
    return self._isEmpty ? "" : string;
  }

  let styler = self._styler;

  if (styler === undefined) {
    return string;
  }

  const { openAll, closeAll } = styler;
  if (string.indexOf("\u001B") !== -1) {
    while (styler !== undefined) {
      // Replace any instances already present with a re-opening code
      // otherwise only the part of the string until said closing code
      // will be colored, and the rest will simply be 'plain'.
      string = stringReplaceAll(string, styler.close, styler.open);

      styler = styler.parent;
    }
  }

  // We can move both next actions out of loop, because remaining actions in loop won't have
  // any/visible effect on parts we add here. Close the styling before a linebreak and reopen
  // after next line to fix a bleed issue on macOS: https://github.com/chalk/chalk/pull/92
  const lfIndex = string.indexOf("\n");
  if (lfIndex !== -1) {
    string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
  }

  return openAll + string + closeAll;
};

let template;
const chalkTag = (chalk, ...strings) => {
  const [firstString] = strings;

  if (!isArray(firstString) || !isArray(firstString.raw)) {
    // If chalk() was called by itself or with a string,
    // return the string itself as a string.
    return strings.join(" ");
  }

  const arguments_ = strings.slice(1);
  const parts = [firstString.raw[0]];

  for (let i = 1; i < firstString.length; i++) {
    parts.push(
      String(arguments_[i - 1]).replace(/[{}\\]/g, "\\$&"),
      String(firstString.raw[i])
    );
  }

  if (template === undefined) {
    template = require("./templates");
  }

  return template(chalk, parts.join(""));
};

Object.defineProperties(Chalk.prototype, styles);

const chalk = Chalk(); // eslint-disable-line new-cap
chalk.supportsColor = stdoutColor;
chalk.stderr = Chalk({ level: stderrColor ? stderrColor.level : 0 }); // eslint-disable-line new-cap
chalk.stderr.supportsColor = stderrColor;

module.exports = chalk;
